Passed
Pull Request — master (#6)
by Pawel
03:03
created

GraphHelpers.ts ➔ createZoom   A

Complexity

Conditions 1

Size

Total Lines 18
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 18
rs 9.7
c 0
b 0
f 0
cc 1
1
import * as d3 from 'd3';
2
import { DependencyLink, DependencyNode } from '../../components/types';
3
import { Simulation } from 'd3';
4
5
export type GraphContainer = d3.Selection<SVGSVGElement, DependencyNode, Element, HTMLElement>;
6
7
export function generateLinkPath(this: Element, link: DependencyLink): void {
8
    const sourceLabelWidth = getNodeDimension(link.source).width;
9
    const targetLabelWidth = getNodeDimension(link.target).width;
10
11
    if (!link.source.x || !link.source.y || !link.target.x || !link.target.y) {
12
        return;
13
    }
14
15
    const xDiff = link.source.x - link.target.x;
16
    const yDiff = link.source.y - link.target.y;
17
18
    const angle = Math.abs(Math.atan(yDiff / xDiff));
19
20
    const isSourceOnTheLeft = xDiff < 0;
21
    const isSourceBelowTarget = yDiff > 0;
22
    const cosinus = Math.cos(angle);
23
    const sinus = Math.sin(angle);
24
25
    const offsetXLeft = -50 * cosinus;
26
    const offsetYBelow = -50 * sinus - 5;
27
28
    const sourceNewX = isSourceOnTheLeft ? (sourceLabelWidth + 20) * cosinus : offsetXLeft;
29
    const sourceNewY = isSourceBelowTarget ? offsetYBelow : 50 * sinus;
30
31
    const targetNewX = isSourceOnTheLeft ? offsetXLeft : (targetLabelWidth + 20) * cosinus;
32
    const targetNewY = isSourceBelowTarget ? 50 * sinus : offsetYBelow;
33
34
    d3.select<Element, DependencyLink>(this)
35
        .attr(
36
            'd',
37
            'M' +
38
                (link.source.x + sourceNewX) +
39
                ',' +
40
                (link.source.y + sourceNewY) +
41
                ', ' +
42
                (link.target.x + targetNewX) +
43
                ',' +
44
                (link.target.y + targetNewY)
45
        )
46
        .attr('stroke', 'black');
47
}
48
49
export function generateMarkers(svgContainer: GraphContainer): void {
50
    svgContainer
51
        .append('svg:defs')
52
        .append('svg:marker')
53
        .attr('id', 'provider')
54
        .attr('viewBox', '-5 -5 40 10')
55
        .attr('refX', 15)
56
        .attr('refY', 0)
57
        .attr('markerWidth', 40)
58
        .attr('markerHeight', 40)
59
        .attr('orient', 'auto')
60
        .append('svg:path')
61
        .attr('d', 'M0,-5L20,0L0,5,q10 -5,0 -10')
62
        .attr('fill', 'rgba(44, 47, 51, 0.12)');
63
}
64
65
export function generateLabelPath(this: Node, node: DependencyNode) {
66
    const labelTextWidth = getLabelTextHeight.call(this, 'width');
67
68
    if (labelTextWidth === 0) {
69
        return '';
70
    }
71
72
    const { isConsumer, isProvider } = node;
73
74
    if (isConsumer && isProvider) {
75
        return 'M4.5,35l9.37,14.59L4.5,64.18h' + (labelTextWidth + 45) + 'l9-14.59L' + (labelTextWidth + 49.5) + ',35H4.5z';
76
    }
77
78
    if (isProvider) {
79
        return 'M' + (labelTextWidth + 49.5) + ',35H4.5l9.37,14.59L4.5,64.18h' + (labelTextWidth + 45);
80
    }
81
82
    if (isConsumer) {
83
        return 'M4.5,64.18h' + (labelTextWidth + 45) + 'l9.42-14.59L' + (labelTextWidth + 49.5) + ',35H4.5';
84
    }
85
86
    return 'M4.5,64.18h' + (labelTextWidth + 55) + 'L' + (labelTextWidth + 59.5) + ',35H4.5';
87
}
88
89
export function handleDrag(simulation: Simulation<DependencyNode, DependencyLink>) {
90
    function dragStarted(node: DependencyNode) {
91
        if (!d3.event.active) {
92
            simulation.alphaTarget(0.3).restart();
93
        }
94
        node.fx = node.x;
95
        node.fy = node.y;
96
    }
97
98
    function dragged(node: DependencyNode) {
99
        node.fx = d3.event.x;
100
        node.fy = d3.event.y;
101
    }
102
103
    function dragEnded(node: DependencyNode) {
104
        if (!d3.event.active) {
105
            simulation.alphaTarget(0);
106
        }
107
        node.fx = null;
108
        node.fy = null;
109
    }
110
111
    return d3
112
        .drag<SVGGElement, DependencyNode>()
113
        .on('start', dragStarted)
114
        .on('drag', dragged)
115
        .on('end', dragEnded);
116
}
117
118
export function createSimulation(nodes: DependencyNode[], links: DependencyLink[], width: number, height: number) {
119
    return d3
120
        .forceSimulation(nodes)
121
        .force(
122
            'dependency',
123
            d3
124
                .forceLink<DependencyNode, DependencyLink>(links)
125
                .distance(180)
126
                .id((node: DependencyNode) => node.name)
127
        )
128
        .force('center', d3.forceCenter(width / 2, height / 2))
129
        .force('y', d3.forceY(0.5))
130
        .force('collide', d3.forceCollide(140))
131
        .force('nodeCollide', d3.forceCollide(140));
132
}
133
134
export function createSVGContainer(
135
    container: HTMLDivElement,
136
    width: number,
137
    height: number
138
): d3.Selection<SVGSVGElement, DependencyNode, Element, HTMLElement> {
139
    return d3
140
        .select<Element, DependencyNode>(`#${container.id}`)
141
        .append('svg')
142
        .attr('preserveAspectRatio', 'xMidYMid meet')
143
        .attr('viewBox', `0 0 ${width} ${height}`)
144
        .attr('width', width)
145
        .attr('height', height);
146
}
147
148
export function createZoom(svgContainer: GraphContainer): d3.Selection<SVGGElement, DependencyNode, Element, HTMLElement> {
149
    const zoomLayer = svgContainer.append('g');
150
151
    const zoomed = () => {
152
        zoomLayer.attr('transform', d3.event.transform);
153
    };
154
155
    svgContainer
156
        .call(
157
            d3
158
                .zoom<SVGSVGElement, DependencyNode>()
159
                .scaleExtent([1 / 2, 12])
160
                .on('zoom', zoomed)
161
        )
162
        .on('dblclick.zoom', null);
163
164
    return zoomLayer;
165
}
166
167
export function getLabelTextHeight(this: Node, dim: 'width' | 'height') {
168
    const textNode = d3.select<SVGGElement, DependencyNode>(this.nextSibling as SVGGElement).node();
169
170
    if (!textNode) {
171
        return 0;
172
    }
173
174
    return dim === 'width' ? textNode.getBBox().width : textNode.getBBox().height;
175
}
176
177
export function getNodeDimension(selectedNode: DependencyNode): { width: number; height: number } {
178
    const foundNode = d3
179
        .select<SVGGElement, DependencyNode>('#labels')
180
        .selectAll<SVGGElement, DependencyNode>('g')
181
        .filter((node: DependencyNode) => node.x === selectedNode.x && node.y === selectedNode.y)
182
        .node();
183
    return foundNode ? foundNode.getBBox() : { width: 200, height: 25 };
184
}
185